﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using System.Net;
using System.Windows.Forms;
using System.Threading;
using OpenNETCF.Drawing;

namespace Pozr
{
    public class Map
    {
        protected struct Style
        {
            public static Font mapFont = new Font(FontFamily.GenericSansSerif, 5, FontStyle.Regular);
            public static Pen framePen = new Pen(Color.Black);
            public static Brush mapBrush = new SolidBrush(Color.White);
            public static Brush buttonBg = new SolidBrush(Color.Ivory);
            public static Brush bgColor = new SolidBrush(Color.Black);
            public static Brush markerColor = new SolidBrush(Color.Red);
        }

        private delegate void ShowTextDelegate(string message);
        private delegate void ShowSpotDelegate(int x, int y);

        private MapRefreshDelegate mapRefreshDelegate;
        private ShowTextDelegate drawTextDelegate;

        private Bitmap bmpMapOffscreen;
        private int buttonRadius;
        private Frame zoomFrame;
        private string statusMessage;

        protected class Frame
        {
            public int x, y, width, height;
            public bool show = false;
        }

        /// <summary>
        /// Double-bufferable control
        /// </summary>
        public class PanelMapType : Control
        {
            /// <summary>
            /// Does nothing when control becomes invalid
            /// </summary>
            /// <param name="e"></param>
            protected override void OnPaintBackground(PaintEventArgs e)
            {
                // do nothing...
            }

            protected override void OnResize(EventArgs e)
            {
                base.OnResize(e);
                Middleware.Map.Resize();
            }
        }

        public class ButtonType : Control
        {
            // private Image normalImage;
            // private Image pressedImage;

            private Image bgImage;

            public ButtonType(string _name, ref Image _bgImage, Control _parent, MouseEventHandler _onMouseDown)
            {
                this.Name = _name;
                this.bgImage = _bgImage;
                this.Parent = _parent;
                this.MouseDown += new System.Windows.Forms.MouseEventHandler(_onMouseDown);
                this.Show();
            }

            /// <summary>
            /// Does nothing when control becomes invalid
            /// </summary>
            /// <param name="e"></param>
            protected override void OnPaintBackground(PaintEventArgs e)
            {/*
                try
                {
                    ImageAttributes attr = new ImageAttributes();
                    Bitmap temp = new Bitmap(this.bgImage);
                    attr.SetColorKey(temp.GetPixel(this.Location.X, this.Location.Y), temp.GetPixel(this.Location.X, this.Location.Y));

                    e.Graphics.DrawImage(bgImage, new Rectangle(0, 0, this.Width, this.Height),
                        this.Location.X, this.Location.Y, this.Width, this.Height, GraphicsUnit.Pixel,
                        attr);
                }
                catch (Exception ex)
                {
                    System.Windows.Forms.MessageBox.Show(ex.Message);
                }*/

                e.Graphics.FillRectangle(Style.buttonBg, 0, 0, this.Width, this.Height);

                e.Graphics.FillEllipse(new SolidBrush(Color.Red), Convert.ToInt32(Size.Width / 2) - 4, Convert.ToInt32(Size.Height / 2) - 4, 8, 8); 
            }

            protected override void OnPaint(PaintEventArgs e)
            {
                base.OnPaint(e);
            }

            public void SendTo(int _x, int _y)
            {
                this.Location = new Point(_x, _y);

                //TODO: animates the button to the specified position... (separate thread)
            }
        }

        private PanelMapType panelMap;
        public PanelMapType PanelMap
        {
            get { return panelMap; }
        }

        private ButtonType buttonLeft, buttonRight, buttonUp, buttonDown;

        //Declaration of delegates
        public MapRefreshDelegate mapRefreshCallback;

        //Declaration of internal variables
        int width, height, zoom;
        double centerLatitude, centerLongitude;
        protected Image currentMap;

        //Declaration of constants
        private readonly System.Globalization.CultureInfo googleCultureInfo = new System.Globalization.CultureInfo("en-US");
        private readonly static int offset = 268435456;
        private readonly static double radius = offset / Math.PI;

        /// <summary>
        /// Width of the map
        /// </summary>
        public int Width
        {
            get { return width; }
        }

        /// <summary>
        /// Height of the map
        /// </summary>
        public int Height
        {
            get { return height; }
        }

        /// <summary>
        /// Stores the current map as an image
        /// </summary>
        public Image CurrentMap
        {
            get { return currentMap; }
        }

        public void DrawText(string s)
        {
            this.panelMap.Invoke(drawTextDelegate, new object[] { s });
        }

        public void DrawFrame(int x, int y, int width, int height, ref Graphics gx)
        {
            gx.DrawRectangle(Style.framePen, x, y, width, height);
        }

        public void DrawMarker(Point p, int radius, ref Graphics gx)
        {
            int offset = Convert.ToInt32(radius / 2);
            gx.FillEllipse(Style.markerColor, p.X - offset, p.Y - offset, radius, radius);
        }

        /// <summary>
        /// Constructor of the Map Class
        /// </summary>
        public Map()
        {
            Middleware.Settings.InitSettingsGroup("Map");
            Middleware.Settings.AddSetting("Map", "ApiKey", "ABQIAAAA6Niw-YBYygCHqgUMmojuvxT2yXp_ZAY8_ufC3CFXhHIE1NvwkxRDEsZnQx-sP7dsUYQYJDjh_LRJtA");

            this.zoom = 2;
            this.panelMap = new PanelMapType();
            this.panelMap.BackColor = System.Drawing.Color.Blue;
            this.panelMap.Dock = System.Windows.Forms.DockStyle.Fill;
            this.panelMap.Name = "panelMap";
            this.panelMap.Paint += new System.Windows.Forms.PaintEventHandler(this.panelMap_Paint);
            this.panelMap.MouseDown += new System.Windows.Forms.MouseEventHandler(this.panelMap_MouseDown);
            this.panelMap.Show();

            this.width = panelMap.Width;
            this.height = panelMap.Height;

            mapRefreshDelegate = new MapRefreshDelegate(panelMap.Refresh);
            drawTextDelegate = new ShowTextDelegate(this.ShowTextHandler);

            bmpMapOffscreen = new Bitmap(this.width, this.height);

            this.currentMap = bmpMapOffscreen;

            mapRefreshCallback = new MapRefreshDelegate(UpdateMap);

            buttonLeft = new ButtonType("BLeft", ref this.currentMap, (Control)this.panelMap, this.buttonLeft_MouseDown);
            buttonRight = new ButtonType("BRight", ref this.currentMap, (Control)this.panelMap, this.buttonRight_MouseDown);
            buttonUp = new ButtonType("BUp", ref this.currentMap, (Control)this.panelMap, this.buttonUp_MouseDown);
            buttonDown = new ButtonType("BDown", ref this.currentMap, (Control)this.panelMap, this.buttonDown_MouseDown);

            zoomFrame = new Frame();
        }

        private void buttonLeft_MouseDown(object sender, MouseEventArgs e)
        {
            SetCenter(0, Convert.ToInt32(height / 2));
            BeginDownload();
        }

        private void buttonRight_MouseDown(object sender, MouseEventArgs e)
        {
            SetCenter(width, Convert.ToInt32(height / 2));
            BeginDownload();
        }

        private void buttonUp_MouseDown(object sender, MouseEventArgs e)
        {
            SetCenter(Convert.ToInt32(width / 2), 0);
            BeginDownload();
        }

        private void buttonDown_MouseDown(object sender, MouseEventArgs e)
        {
            SetCenter(Convert.ToInt32(width / 2), height);
            BeginDownload();
        }

        private void panelMap_MouseDown(object sender, MouseEventArgs e)
        {
            if (zoom < 14)
            {
                int woffset = Convert.ToInt32(width / 4);
                int hoffset = Convert.ToInt32(height / 4);

                zoomFrame.x = e.X - woffset;
                zoomFrame.y = e.Y - hoffset;
                zoomFrame.width = woffset * 2;
                zoomFrame.height = hoffset * 2;
                zoomFrame.show = true;

                panelMap.Refresh();

                SetCenter(e.X, e.Y);
                zoom += 2;
                BeginDownload();
            }
        }

        private void panelMap_Paint(object sender, PaintEventArgs e)
        {
            Graphics gxOff = Graphics.FromImage(bmpMapOffscreen);
            gxOff.Clear(Color.White);
            gxOff.DrawImage(Middleware.Map.CurrentMap, 0, 0);

            int x1 = LonToX(GetLongitude(1));
            int x2 = LonToX(GetLongitude(width));
            int y1 = LatToY(GetLatitude(1));
            int y2 = LatToY(GetLatitude(height));

            if (statusMessage == null)
            {
                foreach (Marker marker in Middleware.Markers.MarkerList)
                {
                    if (LonToX(marker.Longitude) > x1 && LonToX(marker.Longitude) < x2 && LatToY(marker.Latitude) > y1 && LatToY(marker.Latitude) < y2)
                        DrawMarker(ConvertToMapPoint(marker.Latitude, marker.Longitude), 20, ref gxOff);
                }
            }

            if (zoomFrame.show)
            {
                DrawFrame(zoomFrame.x, zoomFrame.y, zoomFrame.width, zoomFrame.height, ref gxOff);
            }

            if (statusMessage != null)
            {
                int offset = Convert.ToInt32(height) - 25;
                gxOff.FillRectangle(new SolidBrush(Color.Gray), 0, offset, panelMap.Width, 20);
                gxOff.DrawString(statusMessage, Style.mapFont, Style.mapBrush, 10, offset + 3);
                statusMessage = null;
                zoomFrame.show = false;
            }

            gxOff.Dispose();
            e.Graphics.DrawImage(bmpMapOffscreen, 0, 0);
        }

        public void UpdateMap()
        {
            this.panelMap.Invoke(mapRefreshDelegate);
        }

        public void Resize()
        {
            if (panelMap.Width <= 640) width = panelMap.Width;
            else width = 640;
            if (panelMap.Height <= 640) height = panelMap.Height;
            else height = 640;

            this.currentMap = new Bitmap(width, height);
            this.bmpMapOffscreen = new Bitmap(panelMap.Width, panelMap.Height);

            if (width < height)
                this.buttonRadius = Convert.ToInt32(width / 12);
            else
                this.buttonRadius = Convert.ToInt32(height / 12);

            this.buttonLeft.Bounds = new Rectangle(5, Convert.ToInt32(height / 2) - buttonRadius, 2 * buttonRadius, 2 * buttonRadius);
            this.buttonRight.Bounds = new Rectangle(width - 5 - 2 * buttonRadius, Convert.ToInt32(height / 2) - buttonRadius, 2 * buttonRadius, 2 * buttonRadius);
            this.buttonUp.Bounds = new Rectangle(Convert.ToInt32(width / 2) - buttonRadius, 5, 2 * buttonRadius, 2 * buttonRadius);
            this.buttonDown.Bounds = new Rectangle(Convert.ToInt32(width / 2) - buttonRadius, height - 30 - 2 * buttonRadius, 2 * buttonRadius, 2 * buttonRadius);

            if (panelMap.Visible) BeginDownload();
        }

        /// <summary>
        /// Converts longitude to x coordinate
        /// </summary>
        /// <param name="lon">Decimal longitude</param>
        /// <returns>X coordinate</returns>
        public static int LonToX(double lon)
        {
            return (int)Math.Round(offset + radius * lon * Math.PI / 180);
        }
        /// <summary>
        /// Converts latitude to y coordinate
        /// </summary>
        /// <param name="lat">Decimal latitude</param>
        /// <returns>Y coordinate</returns>
        public static int LatToY(double lat)
        {
            return (int)Math.Round(offset - radius * Math.Log((1 + Math.Sin(lat * Math.PI / 180)) / (1 - Math.Sin(lat * Math.PI / 180))) / 2);
        }
        /// <summary>
        /// Converts x coordinate to longitude
        /// </summary>
        /// <param name="x">X coordinate</param>
        /// <returns>Decimal longitude</returns>
        public static double XToLon(int x)
        {
            return ((x - offset) / radius) * 180 / Math.PI;
        }
        /// <summary>
        /// Converts y coordinate to latitude
        /// </summary>
        /// <param name="y">Y coordinate</param>
        /// <returns>Decimal latitude</returns>
        public static double YToLat(int y)
        {
            return (Math.PI / 2 - 2 * Math.Atan(Math.Exp((y - offset) / radius))) * 180 / Math.PI;
        }
        /// <summary>
        /// Converts a geographical position into screen coords
        /// </summary>
        /// <param name="lat">Latitude</param>
        /// <param name="lon">Longitude</param>
        /// <returns>Point(x,y) screen coordinate</returns>
        public Point ConvertToMapPoint(double lat, double lon)
        {
            int X = LonToX(lon);
            int CX = LonToX(centerLongitude);
            int Y = LatToY(lat);
            int CY = LatToY(centerLatitude);
            int COX = (int)Math.Round((double)(width / 2));
            int COY = (int)Math.Round((double)(height / 2));
            int DX = (int)Math.Round((double)((X - CX) >> (21 - (int)zoom)));
            int DY = (int)Math.Round((double)((Y - CY) >> (21 - (int)zoom)));
            return new Point(COX + DX, COY + DY);
        }
        /// <summary>
        /// Converts degrees to radian
        /// </summary>
        /// <param name="angle">Angle in degrees</param>
        /// <returns>Angle in radian</returns>
        private double DegreeToRadian(double angle)
        {
            return Math.PI * angle / 180.0;
        }

        private double GetLongitude(int x)
        {
            int DX = ((x - width / 2) << (21 - (int)zoom));
            return XToLon(LonToX(centerLongitude) + DX);
        }

        private double GetLatitude(int y)
        {
            int DY = ((y - height / 2) << (21 - (int)zoom));
            return YToLat(LatToY(centerLatitude) + DY);
        }

        /// <summary>
        /// Set the center of the map to the given screen coordinates
        /// </summary>
        /// <param name="x">X coordinate</param>
        /// <param name="y">Y coordinate</param>
        public void SetCenter(int x, int y)
        {
            int DX = ((x - width / 2) << (21 - (int)zoom));
            int DY = ((y - height / 2) << (21 - (int)zoom));

            centerLongitude = XToLon(LonToX(centerLongitude) + DX);
            centerLatitude = YToLat(LatToY(centerLatitude) + DY);
        }

        /// <summary>
        /// Begins to download the map with the corresponding parameters
        /// </summary>
        public void BeginDownload()
        {
            DrawText("Downloading...");
            Middleware.WriteLog("Downloading map...");
            if (zoom == 1)
            {
                centerLatitude = 0;
                centerLongitude = 0;
            }

            string url = "http://maps.google.com/staticmap?";
            url += "size=" + width.ToString() + "x" + height.ToString();
            url += "&maptype=mobile";
            url += "&center=" + centerLatitude.ToString("F6", googleCultureInfo) + "," + centerLongitude.ToString("F6", googleCultureInfo);
            url += "&zoom=" + zoom.ToString();
            url += "&key=" + Middleware.Settings.GetSetting("Map", "ApiKey").ToString();

            try
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                request.Credentials = CredentialCache.DefaultCredentials;
                request.Timeout = 10000;

                Middleware.RequestState requestState = new Middleware.RequestState(request, new object(), url);
                IAsyncResult result = request.BeginGetResponse(new AsyncCallback(DownloadCallback), requestState);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }

        /// <summary>
        /// Handles the event when the map download completes
        /// Refreshes map and modifies the GUI
        /// </summary>
        /// <param name="result">Event result</param>
        public void DownloadCallback(IAsyncResult result)
        {
            try
            {
                Middleware.RequestState requestState = (Middleware.RequestState)result.AsyncState;
                WebRequest request = (WebRequest)requestState.Request;
                HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);
                System.IO.Stream bitmapStream = (System.IO.Stream)response.GetResponseStream();
                currentMap = new System.Drawing.Bitmap(bitmapStream);
                mapRefreshCallback();
                bitmapStream.Close();
                response.Close();
                Middleware.WriteLog("Map download completed.");
                statusMessage = null;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                DrawText("Download failed.");
                Middleware.WriteLog("Map download failed (" + ex.Message + ")");
            }
        }

        private void ShowTextHandler(string text)
        {
            statusMessage = text;
            panelMap.Refresh();
        }
    }
}
